
#include "wgl.h"
#include <iostream>
#include <cassert>
#include <string>

#if defined(_DEBUG)
	#define WGL_CHECK_CALLING_THREAD \
	do { \
		if(hThread != GetCurrentThread()) \
			throw WglException("WGLWindow accessed by wrong thread!");\
	} while(0)
#else
	#define WGL_CHECK_CALLING_THREAD do {} while(0)
#endif

/*** List of all instances, grouped by thread. ***/
WGLWindow::WindowListStruct WGLWindow::windowList;

/***
	Initialise static members.
***/
void WGLWindow::StaticInitialise() {

	windowList.mutex = CreateMutex(NULL,FALSE,NULL);
}

/***
	Cleanup static members.
***/
void WGLWindow::StaticDispose() {

	WaitForSingleObject( windowList.mutex, INFINITE );

	CloseHandle( windowList.mutex );

	windowList.mutex = NULL;
}

/***
	Constructor:
		Parameters: 1) name UNIQUE window name.
					2) adapter ( 0 for first monitor, 1 for second, etc etc )
					3) width of window drawing area in pixels.
					4) height of window drawing area in pixels.
					5) fullscreen - note: width/height must be a valid fullscreen resolution.
					6) override window procedure, if null virtual WGLWindow::WindowMessageProcessor() will be used
***/
WGLWindow::WGLWindow(const LPCWSTR name,int adapter, int width, int height, bool fullscreen, WNDPROC windowProc)
	:	name		( name ),
		adapter		( adapter ),
		fullscreen	( fullscreen ),
		windowProc	( windowProc ),
		hMonitor	( 0 ),
		hWindow		( 0 ),
		hDC			( 0 ),
		hRC			( 0 ),
		hThread		( 0 ),
		width		( width ),
		height		( height ),
		isCurrent	( false ),
		closeFlag	( false ),

		wglChoosePixelFormatARB( NULL ),
		wglCreateContextAttribsARB( NULL )
{
	memset(&monitorRect,0, sizeof RECT);

	if(!this->windowProc)
		this->windowProc = &GlobalWindowMessageProcessor;

	Initialise();
}

/***
	Destructor:
		Must be called by the thread that created the instance.
***/
WGLWindow::~WGLWindow() throw() {

	Dispose();
}

/***
	MakeCurrent()
	Parameters 1) 'true'  to become the current render context.
				  'false' to reset the current render context.
	
	When a window becomes the current render context, 
	the previous window is informed that it no-longer 
	has the current render context via the 
	WGLWindow::InformNolongerCurrent() function.
***/
void WGLWindow::MakeCurrent( bool current ) {

	WGL_CHECK_CALLING_THREAD

	if(isCurrent == current)
		return;

	if(current)
	{
		WaitForSingleObject( windowList.mutex, INFINITE );

		WindowListStruct::WindowVector &vec =
			windowList.windowVectorMap[hThread];

		ReleaseMutex( windowList.mutex );

		for(WindowListStruct::WindowVector::iterator it  = vec.begin();
													 it != vec.end();
													 it++)
		{
			WGLWindow *window = *it;
			if(window->isCurrent)
				window->InformNolongerCurrent();
		}

		wglMakeCurrent(hDC,hRC);
	}
	else
		wglMakeCurrent(NULL,NULL);

	isCurrent = current;
}

void WGLWindow::Present()
{
	WGL_CHECK_CALLING_THREAD

	SwapBuffers( hDC );
}

void WGLWindow::InformNolongerCurrent() {

	isCurrent = false;
}

int WGLWindow::GetMonitorCount() {

	MonitorEnumStruct data(NULL);

	EnumDisplayMonitors(NULL,NULL,MonitorEnumProc, (LPARAM)&data);

	return data.iteration;
}


// dummy window - we need a valid gl context for making use of wgl extensions.
WGLWindow::WGLWindow(const LPCWSTR name,int adapter,WNDPROC windowProc)
	:	name		( name ),
		adapter		( adapter ),
		windowProc	( windowProc ),
		fullscreen	( false ),
		hMonitor	( 0 ),
		hWindow		( 0 ),
		hDC			( 0 ),
		hRC			( 0 ),
		hThread		( 0 ),
		width		( 400 ),
		height		( 300 ),
		isCurrent	( false ),
		closeFlag	( false ),

		wglChoosePixelFormatARB( NULL ),
		wglCreateContextAttribsARB( NULL )
{
	memset(&monitorRect,0, sizeof RECT);

	if(!this->windowProc)
		this->windowProc = &GlobalWindowMessageProcessor;

	hThread = GetCurrentThread();

	GetMonitor();
	SetResolution();
	InitialiseWindow();
	InitialiseDummyPixelFormat();
	CreateContext();
	MakeCurrent(true);
}

void WGLWindow::InitialiseWgl() {

	WGLWindow *temp = new WGLWindow(name,adapter,windowProc);

	wglChoosePixelFormatARB		= (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress( "wglChoosePixelFormatARB" );
	wglCreateContextAttribsARB	= (PFNWGLCREATECONTEXTATTRIBSARBPROC)wglGetProcAddress( "wglCreateContextAttribsARB" );

	delete temp;

	if(!wglChoosePixelFormatARB)
		throw WglException("wglGetProcAddress( \"wglChoosePixelFormatARB\" ) == NULL");
}

void WGLWindow::InitialiseWindow() {

	RegisterWindowClass( name );

	const RECT &rect = monitorRect;

	HINSTANCE hInstance = GetModuleHandle(NULL);

	hWindow = CreateWindowEx(
		GetExStyle(),
		this->name,
		this->name,
		GetStyle(),
		rect.left, rect.top,
		(rect.right - rect.left) +1,
		(rect.bottom - rect.top) +1,
		NULL,
		NULL,
		hInstance,
		NULL);

	if(!hWindow)
	{
		std::string errStr = GetLastWindowsError();

		throw WglException( errStr );
	}

	hDC = GetDC( hWindow );
}

void WGLWindow::InitialisePixelFormat() {

	int chosenFmt=1;
	UINT chosenCount=1;
	wglChoosePixelFormatARB(hDC, GetPixelAttributes(), NULL, 1, &chosenFmt, &chosenCount);

	if(chosenCount == -1)
		throw WglException("no suitable pixel formats");

	PIXELFORMATDESCRIPTOR *pfd = new PIXELFORMATDESCRIPTOR;
	SetPixelFormat(hDC, chosenFmt, pfd);
	delete pfd;
}

void WGLWindow::InitialiseDummyPixelFormat() {

	int chosenFmt=1;
	PIXELFORMATDESCRIPTOR *pfd = new PIXELFORMATDESCRIPTOR;
	if(SetPixelFormat(hDC, chosenFmt, pfd) == FALSE)
		throw WglException("failed to set dummy pixel format");
	delete pfd;
}

void WGLWindow::CreateContext( HGLRC hShareContext ) {

	if(hShareContext && !wglCreateContextAttribsARB)
		throw WglException("wglCreateContextAttribsARB is requred for sharedContexts");

	if(wglCreateContextAttribsARB) {

		static const int attribList[] = 
		{
			WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
			WGL_CONTEXT_MINOR_VERSION_ARB, 2,
			WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB,
			WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
			0,
		};

		hRC = wglCreateContextAttribsARB(hDC, hShareContext, attribList);

		if(!hRC)
			throw WglException("failed to create opengl-3.2 context");
	}
	else {
		hRC = wglCreateContext( hDC );

		if(!hRC)
			throw WglException("failed to create opengl context");
	}
}

void WGLWindow::Initialise() {

	hThread = GetCurrentThread();

	InitialiseWgl();
	GetMonitor();
	SetResolution();
	InitialiseWindow();
	InitialisePixelFormat();
	CreateContext();
	MakeCurrent(true);
	ShowWindow( hWindow, SW_SHOW );
	UpdateWindow( hWindow );
	SetForegroundWindow( hWindow );
	SetFocus( hWindow );

	WaitForSingleObject( windowList.mutex, INFINITE );

	WindowListStruct::WindowVector &vec =
		windowList.windowVectorMap[hThread];

	vec.push_back( this );

	ReleaseMutex( windowList.mutex );
}

void WGLWindow::Update() {

	MSG msg;

	if(PeekMessage( &msg, hWindow,0,0,PM_REMOVE ) )
	{
		if(msg.message == WM_QUIT)
		{
			closeFlag = true;
			return;
		}

		TranslateMessage( &msg );
		DispatchMessage(  &msg );
	}
}

LRESULT WGLWindow::WindowMessageProcessor(HWND window, UINT message, WPARAM wParam, LPARAM lParam) {

	switch(message)
	{
	default:
		return(DefWindowProc(window, message, wParam, lParam));
	case WM_DESTROY:
		PostQuitMessage(0);
		return(0);
	case WM_KEYDOWN:
		if(wParam == VK_ESCAPE)
			PostQuitMessage(0);
		return(0);
	}
}

LRESULT CALLBACK WGLWindow::GlobalWindowMessageProcessor(HWND hWindow, UINT message, WPARAM wParam, LPARAM lParam) {

	WGLWindow *window = NULL;

	WaitForSingleObject( windowList.mutex, INFINITE );

	// iterate all windows from all threads
	for(WindowListStruct::WindowVectorMap::iterator threadIt  = windowList.windowVectorMap.begin();
													threadIt != windowList.windowVectorMap.end();
													threadIt++)
	{
		WindowListStruct::WindowVector &vec = threadIt->second;

		for(WindowListStruct::WindowVector::iterator windowIt  = vec.begin();
													 windowIt != vec.end();
													 windowIt++)
		{
			window = *windowIt;

			if(window->hWindow == hWindow)
			{
				ReleaseMutex( windowList.mutex );

				return window->WindowMessageProcessor(hWindow, message, wParam, lParam);
			}
		}
	}

	ReleaseMutex( windowList.mutex );

	return(DefWindowProc(hWindow, message, wParam, lParam));
}

void WGLWindow::SetResolution()
{
	if(this->fullscreen == true)
	{
		DEVMODE dm;
		memset(&dm, 0, sizeof dm);
		dm.dmSize = sizeof dm;
		dm.dmBitsPerPel = 32;
		dm.dmPelsWidth = this->width;
		dm.dmPelsHeight = this->height;
		dm.dmPosition.x = monitorRect.left;
		dm.dmPosition.y = monitorRect.top;
		dm.dmFields = DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT|DM_POSITION;

		if(ChangeDisplaySettings(&dm, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			throw WglException("failed to change screen resolution");

			this->width  =  (monitorRect.right - monitorRect.left) + 1;
			this->height =  (monitorRect.bottom - monitorRect.top) + 1;
		}

		GetMonitor(); //re-read monitor info. positions may have changed.
	}
	else
	{
		monitorRect.right = monitorRect.left + width - 1;
		monitorRect.bottom = monitorRect.top + height - 1;
		//AdjustWindowRectEx(&monitorRect, GetStyle(), FALSE, GetExStyle());
	}
}

void WGLWindow::GetMonitor() {

	MonitorEnumStruct data(this);

	hMonitor = 0;

	EnumDisplayMonitors(NULL,NULL,MonitorEnumProc, (LPARAM)&data);

	if(!hMonitor)
		throw WglException("failed to get monitor handle");
}

BOOL WGLWindow::MonitorEnum(HMONITOR hMonitor,HDC hdcMonitor,LPRECT lprcMonitor,MonitorEnumStruct *data) {

	if(data->iteration == this->adapter)
	{
		this->hMonitor		= hMonitor;
		this->monitorRect	= *lprcMonitor;

		return FALSE;
	}
	return TRUE;
}

BOOL CALLBACK WGLWindow::MonitorEnumProc(HMONITOR hMonitor,HDC hdcMonitor,LPRECT lprcMonitor,LPARAM dwData) {

	MonitorEnumStruct *data = (MonitorEnumStruct*)dwData;

	BOOL result = TRUE; 
		
	if(data->caller)
		result = data->caller->MonitorEnum(hMonitor, hdcMonitor, lprcMonitor, data);

	data->iteration++;

	return result;
}


DWORD WGLWindow::GetStyle() const {

	DWORD style = 0;

	if(this->fullscreen)
	{
		style |= WS_POPUP;
	}
	else
	{
		style |= WS_OVERLAPPEDWINDOW;
	}

	return style;
}

DWORD WGLWindow::GetExStyle() const {

	DWORD style = 0;

	if(this->fullscreen)
	{
		style |= WS_EX_TOPMOST;
	}

	return style;
}

const int* WGLWindow::GetPixelAttributes() const {

	/*** Full colour, accelerated openGL compatible pixel format ***/
	static const int pixelAttributes[] = 
	{
		WGL_SUPPORT_OPENGL_ARB,				1,
		WGL_DRAW_TO_WINDOW_ARB,				1,
		WGL_ACCELERATION_ARB,				1,
		WGL_COLOR_BITS_ARB,					24,
		WGL_DEPTH_BITS_ARB,					16,
		WGL_DOUBLE_BUFFER_ARB,				GL_TRUE,
		WGL_SAMPLE_BUFFERS_ARB,				GL_TRUE,
		WGL_SAMPLES_ARB,					8,
		WGL_PIXEL_TYPE_ARB,					WGL_TYPE_RGBA_ARB,
	};

	return pixelAttributes;
}

ATOM WGLWindow::RegisterWindowClass(const LPCWSTR name) const {

	WNDCLASSEX windowClass;

	windowClass.cbSize = sizeof(WNDCLASSEX);
	windowClass.style = CS_VREDRAW | CS_OWNDC;
	windowClass.lpfnWndProc = windowProc;
	windowClass.cbClsExtra = 0;
	windowClass.cbWndExtra = 0;
	windowClass.hInstance = GetModuleHandle(NULL);
	windowClass.hIcon = NULL;
	windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
	windowClass.hbrBackground = NULL;
	windowClass.lpszMenuName = NULL;
	windowClass.lpszClassName = name;
	windowClass.hIconSm = NULL;

	ATOM result = RegisterClassEx(&windowClass);

	if(!result)
		throw WglException("failed to register window class");

	return result;
}

std::string WGLWindow::GetLastWindowsError()
{
	LPVOID lpMsgBuf;
	DWORD dw = GetLastError(); 

	FormatMessageA(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | 
		FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		dw,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPSTR) &lpMsgBuf,
		0, NULL );

	std::string errorString = (char*)lpMsgBuf;

	LocalFree(lpMsgBuf);

	return errorString;
}

bool WGLWindow::ShouldClose() const {

	return closeFlag;
}

void WGLWindow::Dispose() throw() {

	try {
		MakeCurrent(false); // MAY THROW WGL EXCEPTION IN DEBUG MODE ( THREAD MIS-MATCH )
	}
	catch(WglException &ex)
	{
		std::cerr << ex.what() << std::endl;
		if(ex.IsThreadMismatch())
			assert( "WGLWindow destroyed by wrong thread!" && false );
		assert( "WGLWindow::Dispose exception" && false );
	}

	WaitForSingleObject( windowList.mutex, INFINITE );

	WindowListStruct::WindowVector &vec =
		windowList.windowVectorMap[hThread];

	for(WindowListStruct::WindowVector::iterator it  = vec.begin();
		it != vec.end();
		it++)
	{
		WGLWindow *window = *it;
		if(window==this)
		{
			vec.erase( it );
			break;
		}
	}

	ReleaseMutex( windowList.mutex );

	wglDeleteContext( hRC );
	ReleaseDC( hWindow, hDC );
	DestroyWindow( hWindow );
	UnregisterClass( name, GetModuleHandle(NULL));
}

